package nl.unimaas.games.tron.engine;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import nl.unimaas.games.tron.player.Player;

public class Board implements Cloneable, Serializable {

    private static final long serialVersionUID = 1159491603141557778L;
    public final static byte EMPTY_FIELD = 0;
    public final static byte FILLED_FIELD = 11;
    public final static short MAX_BOARD_SIZE = 256;
    public final byte[][] board;
    private int[] playerPositions = new int[0];
    private HashMap<Integer, int[]> startConfigurations = new HashMap<Integer, int[]>();
    /** Name of this board */
    private String name = null;
    private final ArrayList<Integer> moves = new ArrayList<Integer>(4);
    public final static int MOVE_NONE = 0;
    public final static int MOVE_LEFT = -1;
    public final static int MOVE_RIGHT = 1;
    public final static int MOVE_UP = -2;
    public final static int MOVE_DOWN = 2;

    public Board(int w, int h) {
        board = new byte[w][h];
        setPlayerCount(2);
    }

    public Board(int w, int h, int playerCount) {
        board = new byte[w][h];
        setPlayerCount(playerCount);
    }

    public Board(byte[][] board) {
        this.board = board;
    }

    public Board(byte[][] board, int[] playerPositions) {
        this.board = board;
        this.playerPositions = playerPositions;
    }

    protected Board(Board board) {
        this.board = board.board;
        this.playerPositions = board.playerPositions;
    }

    public byte[][] getBoard() {
        return board;
    }

    public int getPlayerCount() {
        return playerPositions.length;
    }

    public int getPlayerPosition(Player p) {
        return playerPositions[p.getNumber()];
    }

    public int getPlayerPosition(int nr) {
        return playerPositions[nr];
    }

    public boolean isValidPosition(int x, int y) {
        return (x >= 0 && y >= 0 && x <= getWidth() && y <= getHeight());
    }

    public boolean isValidPlayerMove(int x, int y, Player p) {
        int pos = getPlayerPosition(p);
        return (getManhattanDistance(x, y, posToX(pos), posToY(pos)) == 1 && isValidPosition(x, y) && isEmpty(x, y));
    }

    public void performMove(Player p, int m) {
        int pos = getPlayerPosition(p);
        performMove(posToX(pos), posToY(pos), m, p.getNumber());
    }

    public void performMove(int playerNr, int m) {
        int pos = getPlayerPosition(playerNr);
        performMove(posToX(pos), posToY(pos), m, playerNr, true);
    }

    public void performMove(int playerNr, int m, boolean check) {
        int pos = getPlayerPosition(playerNr);
        performMove(posToX(pos), posToY(pos), m, playerNr, check);
    }

    public int getPositionFromPlayerMove(Player p, int m) {
        int pos = getPlayerPosition(p);
        return getPositionFromMove(posToX(pos), posToY(pos), m);
    }

    public void performMove(int x, int y, int m, int playerNr) {
        performMove(x, y, m, playerNr, true);
    }

    public void performMove(int x, int y, int m, int playerNr, boolean check) {
        switch (m) {
            case MOVE_LEFT:
                x--;
                break;
            case MOVE_RIGHT:
                x++;
                break;
            case MOVE_UP:
                y--;
                break;
            case MOVE_DOWN:
                y++;
                break;
        }

        if (check) {
            if (isValidPosition(x, y)) {
                if (isEmpty(x, y)) {
                    placePlayerWall(x, y, playerNr);
                } else {
                    throw new IllegalStateException(String.format("move position of P%d contains a wall (%d, %d) %s", playerNr, x, y, m));
                }
            } else {
                throw new IllegalStateException(String.format("invalid position after move to (%d, %d) %s", x, y, m));
            }
        } else {
            placePlayerWall(x, y, playerNr);
        }
    }

    public void placeWall(int x, int y) {
        board[x][y] = FILLED_FIELD;
    }

    public void placePlayerWall(int x, int y, int playerNr) {
        board[x][y] = (byte) (playerNr + 1);
        playerPositions[playerNr] = xyToPos(x, y);
    }

    public void placePlayerWall(int x, int y, Player p) {
        placePlayerWall(x, y, p.getNumber());
    }

    public void removeWall(int x, int y) {
        board[x][y] = EMPTY_FIELD;
    }

    public boolean isEmpty(int x, int y) {
        return (board[x][y] == EMPTY_FIELD);
    }

    public boolean isWall(int x, int y) {
        return (board[x][y] != EMPTY_FIELD);
    }

    public boolean isPlayerWall(int x, int y) {
        return (board[x][y] != EMPTY_FIELD && board[x][y] != FILLED_FIELD);
    }

    public boolean isWallOfPlayer(int x, int y, int playerNr) {
        return (board[x][y] == playerNr);
    }

    public int getField(int x, int y) {
        return board[x][y];
    }

    public boolean isSuicideMove(int sx, int sy, int tx, int ty) {
        if (board[tx][ty] != EMPTY_FIELD) {
            return true;
        }
        if (sx != tx - 1 && board[tx - 1][ty] == EMPTY_FIELD) {
            return false;
        }
        if (sx != tx + 1 && board[tx + 1][ty] == EMPTY_FIELD) {
            return false;
        }
        if (sy != ty - 1 && board[tx][ty - 1] == EMPTY_FIELD) {
            return false;
        }
        if (sy != ty + 1 && board[tx][ty + 1] == EMPTY_FIELD) {
            return false;
        }
        return true;
    }

    public ArrayList<Integer> getValidPlayerMoves(Player p) {
        int pos = getPlayerPosition(p);
        return getValidMoves(posToX(pos), posToY(pos));
    }

    public ArrayList<Integer> getValidMoves(int x, int y) {
        moves.clear();

        if (board[x - 1][y] == EMPTY_FIELD) {
            moves.add(MOVE_LEFT);
        }
        if (board[x][y - 1] == EMPTY_FIELD) {
            moves.add(MOVE_UP);
        }
        if (board[x + 1][y] == EMPTY_FIELD) {
            moves.add(MOVE_RIGHT);
        }
        if (board[x][y + 1] == EMPTY_FIELD) {
            moves.add(MOVE_DOWN);
        }
        return moves;
    }

    /** Excludes suicidial moves */
    public ArrayList<Integer> getValidMovesFiltered(int x, int y) {
        moves.clear();
        if (x > 0) {
            if (board[x - 1][y] == EMPTY_FIELD && !isSuicideMove(x, y, x - 1, y)) {
                moves.add(MOVE_LEFT);
            }
        }
        if (y > 0) {
            if (board[x][y - 1] == EMPTY_FIELD && !isSuicideMove(x, y, x, y - 1)) {
                moves.add(MOVE_UP);
            }
        }
        if (x < getWidth()) {
            if (board[x + 1][y] == EMPTY_FIELD && !isSuicideMove(x, y, x + 1, y)) {
                moves.add(MOVE_RIGHT);
            }
        }
        if (y < getHeight()) {
            if (board[x][y + 1] == EMPTY_FIELD && !isSuicideMove(x, y, x, y + 1)) {
                moves.add(MOVE_DOWN);
            }
        }
        return moves;
    }

    public ArrayList<Integer> getValidPlayerMovePositions(Player p) {
        int pos = getPlayerPosition(p);
        return getValidMovePositions(posToX(pos), posToY(pos));
    }

    public ArrayList<Integer> getValidMovePositions(int x, int y) {
        ArrayList<Integer> pos = moves;
        pos.clear();
        if (x > 0) {
            if (isEmpty(x - 1, y)) {
                pos.add(xyToPos(x - 1, y));
            }
        }
        if (y > 0) {
            if (isEmpty(x, y - 1)) {
                pos.add(xyToPos(x, y - 1));
            }
        }
        if (x < getWidth()) {
            if (isEmpty(x + 1, y)) {
                pos.add(xyToPos(x + 1, y));
            }
        }
        if (y < getHeight()) {
            if (isEmpty(x, y + 1)) {
                pos.add(xyToPos(x, y + 1));
            }
        }
        return pos;
    }
    private final ArrayList<int[]> pos = new ArrayList<int[]>(4);

    public ArrayList<int[]> getValidMovePositionsXY(int x, int y) {
        pos.clear();

        if (isEmpty(x - 1, y)) {
            pos.add(new int[]{x - 1, y});
        }
        if (isEmpty(x, y - 1)) {
            pos.add(new int[]{x, y - 1});
        }
        if (isEmpty(x + 1, y)) {
            pos.add(new int[]{x + 1, y});
        }
        if (isEmpty(x, y + 1)) {
            pos.add(new int[]{x, y + 1});
        }
        return pos;
    }

    public int getValidMoveCount(int x, int y) {
        int count = 0;
        if (isEmpty(x - 1, y)) {
            count++;
        }
        if (isEmpty(x, y - 1)) {
            count++;
        }
        if (isEmpty(x + 1, y)) {
            count++;
        }
        if (isEmpty(x, y + 1)) {
            count++;
        }
        return count;
    }

    public ArrayList<Integer> getValidMovePositions(int x, int y, boolean filterSuicide) {
        ArrayList<Integer> pos = moves;
        pos.clear();
        if (isEmpty(x - 1, y) && (!filterSuicide || !isSuicideMove(x, y, x - 1, y))) {
            pos.add(xyToPos(x - 1, y));
        }
        if (isEmpty(x, y - 1) && (!filterSuicide || !isSuicideMove(x, y, x, y - 1))) {
            pos.add(xyToPos(x, y - 1));
        }
        if (isEmpty(x + 1, y) && (!filterSuicide || !isSuicideMove(x, y, x + 1, y))) {
            pos.add(xyToPos(x + 1, y));
        }
        if (isEmpty(x, y + 1) && (!filterSuicide || !isSuicideMove(x, y, x, y + 1))) {
            pos.add(xyToPos(x, y + 1));
        }
        return pos;
    }

    /** Returns the number of walls adjacent to this cell */
    public int getWallCount(int x, int y) {
        int count = 0;
        if (!isEmpty(x - 1, y)) {
            count++;
        }
        if (!isEmpty(x, y - 1)) {
            count++;
        }
        if (!isEmpty(x + 1, y)) {
            count++;
        }
        if (!isEmpty(x, y + 1)) {
            count++;
        }
        return count;
    }

    public int getFreeCount(int x, int y) {
        return 4 - getWallCount(x, y);
    }

    /** Prepares the board to handle n number of players */
    public void setPlayerCount(int count) {
        playerPositions = new int[count];
    }

    public void setStartPositions(HashMap<Integer, int[]> configurations) {
        startConfigurations = configurations;
    }

    public void setStartPositions(int playerCount, int[] positions) {
        startConfigurations.put(playerCount, positions);
    }

    public int[] getStartPositions(int playerCount) {
        return startConfigurations.get(playerCount);
    }

    public Integer[] getStartPositionConfigurations() {
        return startConfigurations.keySet().toArray(new Integer[startConfigurations.keySet().size()]);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /** 
     * ..X
     * .OX
     * .XX etc.
     *  */
    public boolean isWallhugPosition(int x, int y) {
        if (getFreeCount(x, y) > 2) {
            return false;
        } else if (isEmpty(x - 1, y) && isEmpty(x + 1, y)) {
            return false;
        } else if (isEmpty(x, y - 1) && isEmpty(x, y + 1)) {
            return false;
        } else {
            return true;
        }
    }

    public boolean isArticulationPoint(int x, int y) {
        if (board[x - 1][y] == EMPTY_FIELD && isIsolatingMove(x, y, x - 1, y)) {
            return true;
        } else if (board[x + 1][y] == EMPTY_FIELD && isIsolatingMove(x, y, x + 1, y)) {
            return true;
        } else if (board[x][y - 1] == EMPTY_FIELD && isIsolatingMove(x, y, x, y - 1)) {
            return true;
        } else if (board[x][y + 1] == EMPTY_FIELD && isIsolatingMove(x, y, x, y + 1)) {
            return true;
        }
        return false;
    }

    /** Returns whether we are moving into a position which cuts us off from other move positions */
    public boolean isIsolatingMove(int ox, int oy, int tx, int ty) {
        //o-pos contains more than one free space

        if (getFreeCount(tx, ty) == 1) {
            //check for turns. turns are good for you
            if (ox != tx) {
                if (!isEmpty(tx, ty - 1) && !isEmpty(tx, ty + 1)) {
                    return true;
                }
            } else {
                if (!isEmpty(tx - 1, ty) && !isEmpty(tx + 1, ty)) {
                    return true;
                }
            }
        }

        if (ox != tx) {
            if (!isEmpty(tx, ty - 1) && isEmpty(ox, oy - 1)) {
                return true;
            }
            if (!isEmpty(tx, ty + 1) && isEmpty(ox, oy + 1)) {
                return true;
            }
            return false;
        } else {
            if (!isEmpty(tx - 1, ty) && isEmpty(ox - 1, oy)) {
                return true;
            }
            if (!isEmpty(tx + 1, ty) && isEmpty(ox + 1, oy)) {
                return true;
            }
            return false;
        }
    }

    public int getFreeSpace(int x, int y) {
        byte[][] shadowBoard = Board.cloneArray(board);
        return spaceStep(shadowBoard, x, y);
    }

    public int getFreeSpace(int ox, int oy, int tx, int ty) {
        byte[][] shadowBoard = Board.cloneArray(board);
        shadowBoard[ox][oy] = FILLED_FIELD;
        return spaceStep(shadowBoard, tx, ty);
    }

    private int spaceStep(byte[][] shadowBoard, int x, int y) {
        shadowBoard[x][y] = FILLED_FIELD;
        int space = 1;
        if (shadowBoard[x - 1][y] == EMPTY_FIELD) {
            space += spaceStep(shadowBoard, x - 1, y);
        }
        if (shadowBoard[x + 1][y] == EMPTY_FIELD) {
            space += spaceStep(shadowBoard, x + 1, y);
        }
        if (shadowBoard[x][y - 1] == EMPTY_FIELD) {
            space += spaceStep(shadowBoard, x, y - 1);
        }
        if (shadowBoard[x][y + 1] == EMPTY_FIELD) {
            space += spaceStep(shadowBoard, x, y + 1);
        }
        return space;
    }

    public int getWidth() {
        return board.length;
    }

    public int getHeight() {
        return board[0].length;
    }

    /** Returns a new board with this board's byte array */
    public Board shallowClone() {
        return new Board(board);
    }

    public void print() {
        int x1 = Board.posToX(playerPositions[0]), x2 = Board.posToX(playerPositions[1]);
        int y1 = Board.posToY(playerPositions[0]), y2 = Board.posToY(playerPositions[1]);

        int w = board.length, h = board[0].length;
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                if (x == x1 && y == y1) {
                    System.out.printf("P0 ");
                } else if (x == x2 && y == y2) {
                    System.out.printf("P1 ");
                } else {
                    System.out.printf("%2d ", board[x][y]);
                }
            }
            System.out.println();
        }
    }

    /** Returns a new board with this board's byte array and properties */
    @Override
    public Board clone() {
        Board b = new Board(board);
        b.setStartPositions(startConfigurations);
        b.setName(name);
        b.playerPositions = playerPositions;
        return b;
    }

    /** Returns a new board with a new byte array based on the old one */
    public Board deepClone(boolean inheritProperties) {
        int w = board.length, h = board[0].length;
        byte[][] newBoard = new byte[w][];
        byte[] y;
        for (int x = 0; x < w; x++) {
            y = new byte[h];
            System.arraycopy(board[x], 0, y, 0, h);
            newBoard[x] = y;
        }
        int[] playerPositions = this.playerPositions.clone();
        Board b = new Board(newBoard, playerPositions);
        if (inheritProperties) {
            b.setStartPositions(startConfigurations);
            b.setName(name);
        }
        return b;
    }

    public void copyTo(Board b, boolean inheritProperties) {
        int w = board.length, h = board[0].length;
        if (b.getWidth() != w || b.getHeight() != h) {
            throw new IllegalStateException("boards sizes do not match");
        }

        byte[] y;
        for (int x = 0; x < w; x++) {
            y = b.board[x];
            System.arraycopy(board[x], 0, y, 0, h);
        }
        System.arraycopy(playerPositions, 0, b.playerPositions, 0, 2);

        if (inheritProperties) {
            b.setStartPositions(startConfigurations);
            b.setName(name);
        }
    }

    public byte[][] getClonedBoardArray() {
        int w = board.length;
        int h = board[0].length;
        byte[][] newBoard = new byte[w][];
        byte[] y;
        for (int x = 0; x < w; x++) {
            y = new byte[h];
            System.arraycopy(board[x], 0, y, 0, h);
            newBoard[x] = y;
        }
        return newBoard;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof Board)) {
            return false;
        }
        Board other = (Board) obj;
        if (other.getWidth() != getWidth() || other.getHeight() != getHeight()) {
            return false;
        }
        for (int x = 0; x < getWidth(); x++) {
            for (int y = 0; y < getHeight(); y++) {
                if (board[x][y] != other.board[x][y]) {
                    return false;
                }
            }
        }
        return true;
    }

    public static Board fromFile(String file) throws FileNotFoundException {
        file = file.toLowerCase();
        if (file.endsWith(".txt")) {
            //Google AI Tron map
            FileReader fr = new FileReader(file);
            BufferedReader br = new BufferedReader(fr);
            try {
                //get map size
                String line = br.readLine();
                Pattern numPattern = Pattern.compile("\\d+");
                Matcher numMatcher = numPattern.matcher(line);
                numMatcher.find();
                int w = Integer.parseInt(numMatcher.group());
                numMatcher.find();
                int h = Integer.parseInt(numMatcher.group());
                final byte[][] board = new byte[w][h];
                int[] playerPositions = new int[2];
                //parse level
                for (int y = 0; y < h; y++) {
                    line = br.readLine();
                    if (line != null) {
                        for (int x = 0; x < line.length(); x++) {
                            char c = line.charAt(x);
                            switch (c) {
                                case ' ':
                                    board[x][y] = EMPTY_FIELD;
                                    break;
                                case '#':
                                    board[x][y] = FILLED_FIELD;
                                    break;
                                case '1':
                                    board[x][y] = EMPTY_FIELD;
                                    playerPositions[0] = Board.xyToPos(x, y);
                                    break;
                                case '2':
                                    board[x][y] = EMPTY_FIELD;
                                    playerPositions[1] = Board.xyToPos(x, y);
                                    break;
                                default:
                                    throw new IllegalStateException();
                            }
                        }
                    } else {
                        break;
                    }
                }
                Board b;
                b = new Board(board);
                b.setPlayerCount(2);
                b.setStartPositions(2, playerPositions);
                b.setName(file);
                return b;
            } catch (Exception ex) {
                ex.printStackTrace();
                System.out.println(ex);
                return null;
            } finally {
                try {
                    fr.close();
                } catch (IOException e) {
                }
            }
        } else {
            throw new IllegalArgumentException();
        }
    }

    public static int getMoveFromPositions(int ox, int oy, int tx, int ty) {
        if (ox != tx) {
            if (ox < tx) {
                return MOVE_RIGHT;
            } else {
                return MOVE_LEFT;
            }
        } else {
            if (oy < ty) {
                return MOVE_DOWN;
            } else {
                return MOVE_UP;
            }
        }
    }

    public static int getPositionFromMove(int x, int y, int m) {
        if (m < 0) {
            if (m == Board.MOVE_LEFT) {
                return xyToPos(x - 1, y);
            } else {
                return xyToPos(x, y - 1);
            }
        } else {
            if (m == Board.MOVE_RIGHT) {
                return xyToPos(x + 1, y);
            } else {
                return xyToPos(x, y + 1);
            }
        }
    }

    public static int getManhattanDistance(int x1, int y1, int x2, int y2) {
        return (Math.abs(x1 - x2) + Math.abs(y1 - y2));
    }

    public static int posToX(int boardpos) {
        return boardpos / MAX_BOARD_SIZE;
    }

    public static int posToY(int boardpos) {
        return boardpos % MAX_BOARD_SIZE;
    }

    public static int xyToPos(int x, int y) {
        return x * MAX_BOARD_SIZE + y;
    }

    public static boolean hasAdjacentField(byte[][] board, int x, int y, byte field) {
        return (board[x - 1][y] == field || board[x + 1][y] == field || board[x][y - 1] == field || board[x][y + 1] == field);
    }

    public static boolean hasAdjacentFieldSmallerThan(byte[][] board, int x, int y, byte field) {
        return (board[x - 1][y] < field || board[x + 1][y] < field || board[x][y - 1] < field || board[x][y + 1] < field);
    }

    public static void printBoardArray(byte[][] board) {
        int w = board.length, h = board[0].length;
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                System.out.printf("%4d ", board[x][y]);
            }
            System.out.println();
        }
    }

    public static void printBoardArray(short[][] board) {
        int w = board.length, h = board[0].length;
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                System.out.printf("%5d ", board[x][y]);
            }
            System.out.println();
        }
    }

    public static void printBoardArray(long[][] board) {
        int w = board.length, h = board[0].length;
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                System.out.printf("%5d ", board[x][y]);
            }
            System.out.println();
        }
    }

    public static void printBoardArray(double[][] board) {
        int w = board.length, h = board[0].length;
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                System.out.printf("%5f ", board[x][y]);
            }
            System.out.println();
        }
    }

    public static byte[][] cloneArray(byte[][] source) {
        int w = source.length;
        int h = source[0].length;
        byte[][] newBoard = new byte[w][];
        byte[] y;
        for (int x = 0; x < w; x++) {
            y = new byte[h];
            System.arraycopy(source[x], 0, y, 0, h);
            newBoard[x] = y;
        }
        return newBoard;
    }

    public static int[][] cloneArray(int[][] source) {
        int w = source.length;
        int h = source[0].length;
        int[][] newBoard = new int[w][];
        int[] y;
        for (int x = 0; x < w; x++) {
            y = new int[h];
            System.arraycopy(source[x], 0, y, 0, h);
            newBoard[x] = y;
        }
        return newBoard;
    }

    public static String moveToString(int move) {
        switch (move) {
            case MOVE_NONE:
                return "NONE";
            case MOVE_LEFT:
                return "LEFT";
            case MOVE_RIGHT:
                return "RIGHT";
            case MOVE_UP:
                return "UP";
            case MOVE_DOWN:
                return "DOWN";
            default:
                return "???";
        }
    }
}
